package engine.gui;

import java.awt.FontFormatException;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.util.glu.GLU.*;

import org.lwjgl.util.glu.GLUtessellator;
import org.lwjgl.util.glu.GLUtessellatorCallbackAdapter;

public class Font3D extends GLUtessellatorCallbackAdapter implements Font{
	private static final int INITIAL_FONT_SIZE = 10;
	private static final int BASE_CHARACTER = 32;
	private static final int MAX_CHARACTER = 256;
	
	private double[][] quadraticControlPoints = new double[3][3];
	private double[][] cubicControlPoints = new double[4][3];
	private double[] point = new double[3];
	private double[] coords = new double[6];
	private double[] last = new double[3];
	private double[] start = new double[3];
	
	private int baseList;
	private double[] widths;
	private double[] offsets;
	private double charSpacing;
	private double ascend;
	private double descend;
	private double scale;
	
	public Font3D(String name) {
		java.awt.Font font = new java.awt.Font(name, java.awt.Font.PLAIN, INITIAL_FONT_SIZE);
		init(font);
	}
	
	public Font3D(String name, boolean loadFromDisk) {
		java.awt.Font font = loadFromDisk ? loadFromDisk(name) : new java.awt.Font(name, java.awt.Font.PLAIN, INITIAL_FONT_SIZE);
		init(font);
	}
	
	public Font3D(String name, boolean loadFromDisk, int style) {
		java.awt.Font font = loadFromDisk ? loadFromDisk(name) : new java.awt.Font(name, style, INITIAL_FONT_SIZE);
		init(font);
	}
	
	public Font3D(String name, int style) {
		java.awt.Font font = new java.awt.Font(name, style, INITIAL_FONT_SIZE);
		init(font);
	}
	
	private java.awt.Font loadFromDisk(String fontName) {
		java.awt.Font font = null;
		
		try {
			InputStream inputStream = this.getClass().getResourceAsStream(
					"/" + fontName);
			font = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, inputStream)
					.deriveFont((float)INITIAL_FONT_SIZE);
		} catch (FontFormatException e) {
			e.printStackTrace();
		} catch (IOException e) {
			System.out.println(fontName + " could not be found!");
			font = new java.awt.Font("serif", java.awt.Font.PLAIN, INITIAL_FONT_SIZE);
		}
		
		return font;
	}
	
	private void init(java.awt.Font font) {
		widths = new double[MAX_CHARACTER - BASE_CHARACTER];
		offsets = new double[MAX_CHARACTER - BASE_CHARACTER];
		baseList = glGenLists(MAX_CHARACTER - BASE_CHARACTER);
		
		AffineTransform trans = new AffineTransform();
		BufferedImage buf = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR);
		Graphics2D g = buf.createGraphics();
		FontRenderContext frc = g.getFontRenderContext();
		
		for(int i = BASE_CHARACTER; i < MAX_CHARACTER; i++) {
			createList((char)i, g, font, baseList + i - BASE_CHARACTER, frc, trans);
		}
		
		charSpacing = widths['m' - baseList] * 0.2;
	}
	
	public void draw(String text) {
		draw(text, 0, 0, 0, INITIAL_FONT_SIZE * scale);
	}
	
	public void draw(String text, double size) {
		draw(text, 0, 0, 0, size);
	}
	
	@Override
	public void draw(String text, double posX, double posY) {
		draw(text, posX, posY, 0, INITIAL_FONT_SIZE * scale);
	}
	
	public void draw(String text, double posX, double posY, double posZ) {
		draw(text, posX, posY, posZ, INITIAL_FONT_SIZE * scale);
	}
	
	public void draw(String text, double posX, double posY, double posZ, double size) {
		glPushMatrix();
		glTranslated(posX, posY, posZ);
		double scale = size / INITIAL_FONT_SIZE;
		glScaled(scale, scale, scale);
		for(int i = 0; i < text.length(); i++) {
			glTranslated(offsets[(int)text.charAt(i) - BASE_CHARACTER], 0, 0);
			glCallList(baseList + (int)text.charAt(i) - BASE_CHARACTER);
			glTranslated(widths[(int)text.charAt(i) - BASE_CHARACTER] + charSpacing, 0, 0);
		}
		
		glPopMatrix();
	}
	
	public double getWidth(String text) {
		double width = 0;
		for(int i = 0; i < text.length(); i++) {
			width += offsets[(int)text.charAt(i) - BASE_CHARACTER] + widths[(int)text.charAt(i) - BASE_CHARACTER] + charSpacing;
		}
		return width * scale;
	}
	
	/**
	 * Creates a display list for the char c
	 * @param c
	 * @return
	 */
	private void createList(char c, Graphics2D g, java.awt.Font font, int list, FontRenderContext frc, AffineTransform trans) {
		GlyphVector gv = font.createGlyphVector(frc, c + "");
		Shape shape = gv.getOutline();
		PathIterator iter = shape.getPathIterator(trans);
		
		//setting up used variables
		int lineType;
		
		//setting up tesselator and callback
		GLUtessellator tesselator = gluNewTess();
		tesselator.gluTessCallback(GLU_TESS_BEGIN, this);
		tesselator.gluTessCallback(GLU_TESS_VERTEX, this);
		tesselator.gluTessCallback(GLU_TESS_COMBINE, this);
		tesselator.gluTessCallback(GLU_TESS_END, this);
		tesselator.gluTessCallback(GLU_TESS_ERROR, this);
		
		//setting up winding rule for tesselation object
		switch(iter.getWindingRule()) {
		case PathIterator.WIND_EVEN_ODD:
			tesselator.gluTessProperty(GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_ODD);
			break;
		case PathIterator.WIND_NON_ZERO:
			tesselator.gluTessProperty(GLU_TESS_WINDING_RULE, GLU_TESS_WINDING_NONZERO);
		}
		
		glPushAttrib(GL_LINE_BIT);
		glLineWidth(15.0f);
		
		glNewList(list, GL_COMPILE);
		Object userData = null;
		double[] vertexData = null;
		tesselator.gluTessBeginPolygon(userData);
		while(!iter.isDone()) {
			lineType = iter.currentSegment(coords);
			
			switch(lineType) {
			case PathIterator.SEG_CLOSE:
				tesselator.gluTessEndContour();
				break;
			case PathIterator.SEG_CUBICTO:
				cubicControlPoints[0][0] = last[0];
		        cubicControlPoints[0][1] = last[1];
		        cubicControlPoints[1][0] = coords[0];
		        cubicControlPoints[1][1] = coords[1];
		        cubicControlPoints[2][0] = coords[2];
		        cubicControlPoints[2][1] = coords[3];
		        cubicControlPoints[3][0] = coords[4];
		        cubicControlPoints[3][1] = coords[5];
		        
		        for(int i = 0; i < 10; i++) {
		        	point = cubicPoint(cubicControlPoints, i / 10.0);
		        	vertexData = point;
		        	tesselator.gluTessVertex(point, 0, vertexData);
		        }
		        
		        last[0] = coords[4];
		        last[1] = coords[5];
				break;
			case PathIterator.SEG_LINETO:
				last[0] = coords[0];
				last[1] = coords[1];
				vertexData = last.clone();
				tesselator.gluTessVertex(vertexData, 0, vertexData);
				break;
			case PathIterator.SEG_MOVETO:
				start[0] = coords[0];
				start[1] = coords[1];
				last[0] = coords[0];
				last[1] = coords[1];
				vertexData = last.clone();
				tesselator.gluTessBeginContour();
				tesselator.gluTessVertex(vertexData, 0, vertexData);
				break;
			case PathIterator.SEG_QUADTO:
				quadraticControlPoints[0][0] = last[0];
		        quadraticControlPoints[0][1] = last[1];
		        quadraticControlPoints[1][0] = coords[0];
		        quadraticControlPoints[1][1] = coords[1];
		        quadraticControlPoints[2][0] = coords[2];
		        quadraticControlPoints[2][1] = coords[3];
		        
		        for(int i = 0; i < 10; i++) {
		        	point = quadricPoint(quadraticControlPoints, i / 10.0);
		        	vertexData = point;
		        	tesselator.gluTessVertex(point, 0, vertexData);
		        }
		        
		        last[0] = coords[2];
		        last[1] = coords[3];
				break;
			}
			
			iter.next();
		}
		tesselator.gluTessEndPolygon();
		
		glEndList();
		
		glPopAttrib();
		
		widths[c - BASE_CHARACTER] = shape.getBounds2D().getWidth();
		offsets[c - BASE_CHARACTER] = -shape.getBounds2D().getX();
		
		if(c == 'B') {
			ascend = shape.getBounds2D().getHeight();
		}
		
		if(c == 'g') {
			descend = shape.getBounds2D().getY();
		}	
	}
	
	@Override
	public void begin(int type) {
		glBegin(type);
	}
	
	@Override
	public void beginData(int type, Object polygonData) {
		glBegin(type);
	}
	
	@Override
	public void edgeFlag(boolean boundaryEdge) {
		
	}
	
	@Override
	public void edgeFlagData(boolean boundaryEdge, Object polygonData) {
		
	}
	
	@Override
	public void vertex(Object vertexData) {
		double[] coords = (double[])vertexData;
		glVertex3d(coords[0], coords[1], coords[2]);
	}
	
	@Override
	public void vertexData(Object vertexData, Object polygonData) {
		double[] coords = (double[])vertexData;
		glVertex3d(coords[0], coords[1], coords[2]);
	}
	
	@Override
	public void end() {
		glEnd();
	}
	
	@Override
	public void endData(Object polygonData) {
		glEnd();
	}
	
	@Override
	public void combine(double[] coords, Object[] data, float[] weight,
			Object[] outData) {
		double[] vertex = new double[3];
		for(int i = 0; i < 3; i++) {
			vertex[i] = coords[i];
		}
		outData[0] = vertex;
	}
	
	@Override
	public void combineData(double[] coords, Object[] data, float[] weight,
			Object[] outData, Object polygonData) {
		double[] vertex = new double[3];
		for(int i = 0; i < 3; i++) {
			vertex[i] = coords[i];
		}
		outData[0] = vertex;
	}
	
	@Override
	public void error(int errnum) {
		System.out.println("Tesselation error: " + errnum + " (" + gluErrorString(errnum) + ")");
	}
	
	@Override
	public void errorData(int errnum, Object polygonData) {
		System.out.println("Tesselation error: " + errnum + " (" + gluErrorString(errnum) + ")");
	}
	
	private double[] cubicPoint(double[][] cubic, double t) {
		double[] ret = new double[3];
		
		for(int i = 0; i < ret.length; i++) {
			ret[i] = B(3, 0, t) * cubic[0][i] + B(3, 1, t) * cubic[1][i] + B(3, 2, t) * cubic[2][i] + B(3, 3, t) * cubic[3][i];
		}
		
		return ret;
	}
	
	
	private double[] quadricPoint(double[][] quadric, double t) {
		double[] ret = new double[3];
		
		for(int i = 0; i < ret.length; i++) {
			ret[i] = B(2, 0, t) * quadric[0][i] + B(2, 1, t) * quadric[1][i] + B(2, 2, t) * quadric[2][i];
		}
		
		return ret;
	}
	
	private double B(int n, int m, double t) {
		return C(n, m) * pow(t, m) * pow(1 - t, n - m);
	}
	
	private double C(int n, int m) {
		return faculty(n) / (faculty(m) * faculty(n - m));
	}
	
	private double pow(double base, int exponent) {
		double ret = 1;
		
		for(int i = 0; i < exponent; i++) {
			ret *= base;
		}
		
		return ret;
	}
	
	private double faculty(int n) {
		double ret = 1;
		
		for(int i = 1; i <= n; i++) {
			ret *= i;
		}
		
		return ret;
	}
	
	public double getCharSpacing() {
		return charSpacing;
	}
	
	public void setCharSpacing(double charSpacing) {
		this.charSpacing = charSpacing;
	}
	
	public double getAscend() {
		return ascend * scale;
	}
	
	public double getDescend() {
		return descend * scale;
	}
	
	public void setScale(double scale) {
		this.scale = scale;
	}
	
	public double getScale() {
		return scale;
	}
}
